package codegManager;

import java.io.File;
import java.io.IOException;
import java.util.Hashtable;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;

import serialization.CodegSerializer;

import codeg.Codeg;
import codeg.CodegChangeListener;
import codeg.CodegException;

public class CodegManager {
    private BundleContext bundleContext;
    private ProjectSource projectSource;

    private Hashtable<String, Codeg> codegInstances = new Hashtable<String, Codeg>();
    private Hashtable<String, Class<?>> codegClasses = new Hashtable<String, Class<?>>();
    
    public static final Pattern codegPattern = Pattern.compile("@@(\\w*)\\((\\w*)\\)");

    public CodegManager() {
    }

    public CodegManager(BundleContext bundleContext, ProjectSource projectSource) {
        this.setBundleContext(bundleContext);
        this.setProjectSource(projectSource);
    }

    public Codeg getCodeg(final String expr) throws CodegException {
        // Check if the requested codeg is already loaded
        if (codegInstances.containsKey(expr))
            return codegInstances.get(expr);
        
        if (!CodegManager.isCodegDeclaration(expr))
            // FIXME: this error should not happen because the editor should prevent it
            throw new CodegException("Incorrect syntax", CodegException.SYNTAX_ERROR);
            
        Codeg codeg = null;
        String repr = CodegManager.getCodegRepr(expr);
        String id = CodegManager.getCodegId(expr);

        File codegsDir = new File(getProjectSource().getProjectFolder(), "codegs");
        // Check if the codegsDir exists. In case it doesn't, create it.
        if (!codegsDir.exists())
            codegsDir.mkdir();
        if (!codegsDir.canRead())
            throw new CodegException("Codeg folder cannot be read", CodegException.READ_ERROR);

        Class<?> codegClass;
        codegClass = codegClasses.get(repr);
        // If we can't find a loaded codeg for this representation
        if (codegClass == null) {
            Bundle bundle = null;
            // Search all bundles and try to find the one that implements this codeg based on its representation
            String[] files = codegsDir.list();
            for (String file : files) {
                if (file.endsWith(".jar")) {
                    JarFile jf;
                    Manifest mf;
                    try { 
                        jf = new JarFile(new File(codegsDir, file));
                        mf = jf.getManifest();
                    }
                    catch (IOException e) { continue; }
                    if (!repr.equals(mf.getMainAttributes().getValue("Codeg-Repr")))
                        continue;

                    try {
                        bundle = getBundleContext().installBundle(codegsDir.toURI().resolve(file).toString());
                    }
                    catch (BundleException e) {
                        // If the bundle is already loaded, find it.
                        if (e.getType() == BundleException.DUPLICATE_BUNDLE_ERROR) {
                            Bundle[] bundles = getBundleContext().getBundles();
                            for (Bundle candidate : bundles) {
                                if (repr.equals(candidate.getHeaders().get("Codeg-Repr"))) {
                                    bundle = candidate;
                                    break;
                                }
                            }
                        }
                    }
                }
            }
            
            // If we found a bundle, load it.
            if (bundle != null) {
                try {
                    codegClass = bundle.loadClass((String) bundle.getHeaders().get("Codeg-Class"));
                } catch (ClassNotFoundException e) {}
                codegClasses.put(repr, codegClass);
            }
        }
        
        // Create the codeg instance, loading its stored data if possible
        if (codegClasses.containsKey(repr)) {
            codegClass = codegClasses.get(repr);

            File ifile = new File(codegsDir, expr + ".xml");
            // Load codeg data from file...
            if (ifile.exists()) {
                CodegSerializer serializer;
                try {
                    serializer = (CodegSerializer) codegClass.getMethod("getSerializer").invoke(null);
                }
                catch (Exception e) {
                    throw new CodegException("Error creating codeg", CodegException.UNKNOWN_ERROR);
                }
                codeg = (Codeg) serializer.load(ifile, codegClass);
            }
            // .. or instantiate a new codeg.
            else {
                try {
                    codeg = (Codeg) codegClass.newInstance();
                }
                catch (Exception e) {
                    throw new CodegException("Error creating codeg", CodegException.UNKNOWN_ERROR);
                }
                codeg.setId(id);
            }
            // Register the codeg and listen for changes
            codegInstances.put(expr, codeg);
            codeg.addListener(new CodegChangeListener() {
                public void action() {
                    try {
                        saveCodeg(expr);
                    }
                    catch (CodegException e) {
                        System.err.println(e.getMessage());
                    }
                }
            });
        }
        else {
            throw new CodegException("Codeg not found", CodegException.NOT_FOUND);
        }
        
        return codeg;
    }

    /**
     * Save an open codeg instance with the given declaration expression.
     * 
     * @throws CodegException
     *             if anything goes wrong when saving a codeg instance.
     */
    public void saveCodeg(String expr) throws CodegException {
        Codeg codeg = codegInstances.get(expr);
        if (codeg == null)
            throw new CodegException("Codeg instance could not be found", CodegException.SAVE_ERROR);
        CodegSerializer serializer = null;
        File codegsDir = new File(getProjectSource().getProjectFolder(), "codegs");
        
        try {
            serializer = (CodegSerializer) codeg.getClass().getMethod("getSerializer").invoke(null);
        }
        catch (Exception e) {
            throw new CodegException("Error saving codeg '" + codeg.getId() + "'", CodegException.SAVE_ERROR);
        }
        File ofile = new File(codegsDir, expr + ".xml");
        serializer.save(codeg, ofile);
    }

    public void setBundleContext(BundleContext bundleContext) {
        this.bundleContext = bundleContext;
    }

    public BundleContext getBundleContext() {
        return bundleContext;
    }

    public void setProjectSource(ProjectSource projectSource) {
        this.projectSource = projectSource;
    }

    public ProjectSource getProjectSource() {
        return projectSource;
    }

    public static boolean isCodegDeclaration(String expr) {
        return codegPattern.matcher(expr).matches();
    }

    public static String getCodegRepr(String expr) {
        Matcher m = CodegManager.codegPattern.matcher(expr);
        if (m.find())
            return m.group(1);
        return null;
    }

    public static String getCodegId(String expr) {
        Matcher m = CodegManager.codegPattern.matcher(expr);
        if (m.find())
            return m.group(2);
        return null;
    }
}